package code.google.jcontainer.util;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Load and Hold all the loaded annotation classes
 *
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 */
public class AnnotationRepository {

    private static Log log = LogFactory.getLog(AnnotationRepository.class);

    private AnnotationScanner annotationScanner;
    private URLClassLoader loader;

    /**
     * @param loader the classloader used to load component class
     */
    public AnnotationRepository(URLClassLoader loader) {
        this.loader = loader;
        annotationScanner = new AnnotationScanner(loader);
    }

    /**
     * * scan the specified path  to find annotated class
     *
     * @param paths jars or class directories
     */
    public void scanAnnotation(File... paths) {
        scanAnnotation(paths, ".*");
    }

    /**
     * * scan classpath with the filter packages to find annotated class
     *
     * @param packages filter packages
     */
    public void scanAnnotation(String... packages) {
        scanAnnotation(null, packages);
    }

    /**
     * * scan the specified path with the filter packages to find annotated class
     *
     * @param paths    jars or class directories
     * @param packages filter packages
     */
    public void scanAnnotation(File[] paths, String... packages) {
        List<URL> pathURLs = new ArrayList<URL>();
        if (paths != null && paths.length == 1 && paths[0] == null) { // default use null
            paths = null;
        }
        if (paths != null && paths.length != 0) {
            URL[] classpathURLs = loader.getURLs();
            List<URL> classpathURLList = Arrays.asList(classpathURLs);
            for (File path : paths) {
                URL pathURL = null;
                try {
                    pathURL = path.toURI().toURL();
                }
                catch (Exception e) {
                    throw new RuntimeException(e);
                }
                if (!classpathURLList.contains(pathURL)) {
                    log.warn("The scanning path " + pathURL + " is not in classpath.");
                }
                else {
                    pathURLs.add(pathURL);
                }

                // scan jars in a directory, so that can support WEB-INF/lib
                if (path.isDirectory()) {
                    // only list top dir
                    File[] jarFiles = path.listFiles(FileFilterUtils.and(FileFilterUtils.notDirectoryFileFilter(), FileFilterUtils.suffixFileFilter(".jar")));
                    try {
                        for (File jarFile : jarFiles) {
                            URL jarURL = jarFile.toURI().toURL();
                            if (!classpathURLList.contains(jarURL)) {
                                log.warn("The scanning path " + jarURL + " is not in classpath.");
                            }
                            else {
                                pathURLs.add(jarURL);
                            }
                        }
                    }
                    catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        if (packages == null || packages.length == 0) {
            packages = new String[]{".*"}; // will scan all package
        }

        URL[] pathURLArray = pathURLs.toArray(new URL[pathURLs.size()]);
        log.info("Scanning annotations from " + Arrays.toString(pathURLArray));
        annotationScanner.scanAnnotations(pathURLs.toArray(new URL[pathURLs.size()]), packages);
    }

    /**
     * get all classes that is annotated by this annotation
     */
    public Class<?>[] getAnnotatedClasses(Class<? extends Annotation> annotationClass) {
        return annotationScanner.getAnnotatedClasses(annotationClass);
    }

    /**
     * return the classloader used to load component class
     */
    public URLClassLoader getClassLoader() {
        return loader;
    }

    /**
     * Annotation parse class by using ASM
     */
    public static class AnnotationScanner {

        private URLClassLoader loader;
        /**
         * AnnotationName=>[ClassInfo]
         */
        private final Map<String, List<ClassInfo>> annotatedClasses = new HashMap<String, List<ClassInfo>>();

        AnnotationScanner(URLClassLoader loader) {
            this.loader = loader;
        }

        void scanAnnotations(URL[] jarURLs, String... regexPackageFilter) {
            List<URL> appURLs = new ArrayList<URL>();
            if (jarURLs != null && jarURLs.length != 0) {
                appURLs.addAll(Arrays.asList(jarURLs));
            }
            else {
                // available URLs, filtered jdk internal urls
                URL[] urls = loader.getURLs();
                for (URL url : urls) {
                    URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}, null);
                    // drop rt.jar according to java.lang.Object
                    URL testURL = urlClassLoader.findResource(Object.class.getName().replace(".", "/") + ".class");
                    // charset.jar
                    URL charsetURL = urlClassLoader.findResource("sun/nio/cs/ext/GBK.class");
                    if (testURL == null && charsetURL == null) {
                        appURLs.add(url);
                    }
                }
            }
            Map<String, byte[]> classBytesArray = new HashMap<String, byte[]>();
            // scan Component in urls
            for (URL url : appURLs) {
                try {
//                classNames.addAll(Arrays.asList(FileUtils.getClassNames(url)));
                    classBytesArray.putAll(FileUtils.scanClasses(url, regexPackageFilter));
                }
                catch (IOException e) {
                    throw new RuntimeException("Failed to get Class names from url: " + url.toString(), e);
                }
            }
            for (Map.Entry<String, byte[]> entry : classBytesArray.entrySet()) {
                readClass(entry.getKey(), entry.getValue());
            }
        }

        public Class[] getAnnotatedClasses(Class<? extends Annotation> annotationClass) {
            List<Class> classes = new ArrayList<Class>();
            List<ClassInfo> infos = getAnnotationInfos(annotationClass.getName());
            for (ClassInfo classInfo : infos) {
                try {
                    Class clazz = classInfo.get();
                    // double check via proper reflection
                    if (clazz.isAnnotationPresent(annotationClass)) {
                        classes.add(clazz);
                    }
                }
                catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
            return classes.toArray(new Class[classes.size()]);
        }

        // ----------- ASM bytecode reader ------------

        private void readClass(String className, byte[] classBytes) {
            try {
                ClassReader classReader = new ClassReader(classBytes);
                classReader.accept(new ClassInfoBuildingVisitor(), ClassReader.SKIP_DEBUG);
            }
            catch (Exception e) {
                throw new RuntimeException("Error to read class: " + className, e);
            }
        }

        private List<ClassInfo> getAnnotationInfos(String name) {
            List<ClassInfo> infos = annotatedClasses.get(name);
            if (infos == null) {
                infos = new ArrayList<ClassInfo>();
                annotatedClasses.put(name, infos);
            }
            return infos;
        }

        class ClassInfoBuildingVisitor implements ClassVisitor, AnnotationVisitor {
            private AnnotatableInfo info;

            public ClassInfoBuildingVisitor() {
            }

            public ClassInfoBuildingVisitor(AnnotatableInfo info) {
                this.info = info;
            }

            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                if (!name.endsWith("package-info")) {
                    ClassInfo classInfo = new ClassInfo(javaName(name), javaName(superName));

                    for (String interfce : interfaces) {
                        classInfo.getInterfaces().add(javaName(interfce));
                    }
                    info = classInfo;
                }
            }

            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                AnnotationInfo annotationInfo = new AnnotationInfo(desc);
                info.getAnnotations().add(annotationInfo);
                if (info instanceof ClassInfo) {
                    getAnnotationInfos(annotationInfo.getName()).add((ClassInfo) info);
                }
                return new ClassInfoBuildingVisitor(annotationInfo);
            }

            private String javaName(String name) {
                return (name == null) ? null : name.replace('/', '.');
            }

            //--------- not need to implement

            public void visitAttribute(Attribute attribute) {
            }

            public void visitEnd() {
            }

            public FieldVisitor visitField(int i, String string, String string1, String string2, Object object) {
                return null;
            }

            public void visitInnerClass(String string, String string1, String string2, int i) {
            }

            public MethodVisitor visitMethod(int i, String string, String string1, String string2, String[] strings) {
                return null;
            }

            public void visitOuterClass(String string, String string1, String string2) {
            }

            public void visitSource(String string, String string1) {
            }

            public void visit(String string, Object object) {
            }

            public AnnotationVisitor visitAnnotation(String string, String string1) {
                return null;
            }

            public AnnotationVisitor visitArray(String string) {
                return null;
            }

            public void visitEnum(String string, String string1, String string2) {
            }
        }

        abstract class AnnotatableInfo {
            private final List<AnnotationInfo> annotations = new ArrayList<AnnotationInfo>();

            public AnnotatableInfo() {
            }

            public AnnotatableInfo(AnnotatedElement element) {
                for (Annotation annotation : element.getAnnotations()) {
                    annotations.add(new AnnotationInfo(annotation.annotationType().getName()));
                }
            }

            public List<AnnotationInfo> getAnnotations() {
                return annotations;
            }

            public abstract String getName();
        }

        class ClassInfo extends AnnotatableInfo {
            private final String name;
            private final String superType;
            private final List<String> interfaces = new ArrayList<String>();
            private Class<?> clazz;
            private ClassNotFoundException notFound;

            public ClassInfo(Class clazz) {
                super(clazz);
                this.clazz = clazz;
                this.name = clazz.getName();
                Class superclass = clazz.getSuperclass();
                this.superType = superclass != null ? superclass.getName() : null;
            }

            public ClassInfo(String name, String superType) {
                this.name = name;
                this.superType = superType;
            }

            public String getPackageName() {
                return name.substring(name.lastIndexOf(".") + 1, name.length());
            }

            public List<String> getInterfaces() {
                return interfaces;
            }

            public String getName() {
                return name;
            }

            public String getSuperType() {
                return superType;
            }

            public Class get() throws ClassNotFoundException {
                if (clazz != null) return clazz;
                if (notFound != null) throw notFound;
                try {
                    this.clazz = loader.loadClass(name);
                }
                catch (ClassNotFoundException e) {
                    notFound = e;
                    throw e;
                }
                return clazz;
            }

            public String toString() {
                return name;
            }
        }

        class AnnotationInfo extends AnnotatableInfo {
            private final String name;

            public AnnotationInfo(Annotation annotation) {
                this(annotation.getClass().getName());
            }

            public AnnotationInfo(Class<? extends Annotation> annotation) {
                this.name = annotation.getName().intern();
            }

            public AnnotationInfo(String name) {
                name = name.replaceAll("^L|;$", "");
                name = name.replace('/', '.');
                this.name = name.intern();
            }

            public String getName() {
                return name;
            }

            public String toString() {
                return getName();
            }
        }

    }
}
